import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Stack;

/**
 * 
 * Moises Florez and Peter Matano
 * 
 */
public class OS implements OperatingSystem {
	private Hardware simHW;
	private DiskEntity userSpaceDiskEntity;
	private DiskEntity systemSpaceDiskEntity;
	private ProgramEntity programEntity;
	int ttyData = 1;

	boolean terminalInUse;
	private Stack<Integer> terminalUseStack;

	boolean readyRead;
	boolean readyWrite;

	private int exeCount = 0;

	private List<ConnectionDetails> connectionDetailList;

	private RoundRobinScheduler processScheduler;
	private Process currentProcess;

	private int countdownCount = 8000;
	private int processQuntum = 128;

	private FileList<File> fileList;
	private int processIndex = 0;
	private MemoryManager memoryManager;

	private boolean blocking;
	private int terminalDataStartAddress = -1;
	private int numberOfCharToRead = -1;

	private int maxUsrSpaceBlockCount = 32;

	public OS(Hardware hw) {
		simHW = hw; // Set simulator hardware.
		programEntity = new ProgramEntity();
		userSpaceDiskEntity = new DiskEntity();
		systemSpaceDiskEntity = new DiskEntity();
		terminalUseStack = new Stack<Integer>();
		processScheduler = new RoundRobinScheduler();
		connectionDetailList = new ArrayList<ConnectionDetails>();
		memoryManager = new MemoryManager();
		currentProcess = new Process();
		fileList = new FileList<File>();
	}

	/**
	 * Halt the OS
	 */
	private void haltOS() {
		simHW.store(Hardware.Address.haltRegister, 2);
	}

	/**
	 * Set HW status OK. This start the actual execution command in systemCalls
	 */
	private void setHardwareStatusOK() {
		this.simHW.store(Hardware.Address.systemBase, Hardware.Status.ok);
	}

	/**
	 * Store the connection ID to an undefined value.
	 */
	private void setUndefined() {
		this.simHW.store(Hardware.Address.systemBase + 1, -200000);
	}

	private void setW0UndefinedOnProcessExit() {
		this.simHW.store(Hardware.Address.systemBase, -200000);
	}

	/**
	 * Idle start program.
	 */
	private void setIdleStart() {
		this.simHW.store(Hardware.Address.baseRegister, Hardware.Address.idleStart);
		this.simHW.store(Hardware.Address.topRegister, Hardware.Address.idleEnd);
		this.simHW.store(Hardware.Address.PCRegister, Hardware.Address.idleStart);
	}

	/*
	 * Set count down-timer
	 */
	private void setCountdownStart(int countdownCount) {
		simHW.store(Hardware.Address.countdownRegister, countdownCount);
	}

	/*
	 * Set count down-timer
	 */
	private void setCountdownStart() {
		simHW.store(Hardware.Address.countdownRegister, this.countdownCount);
	}

	/**
	 * OS Interrupt handler
	 */
	@Override
	public void interrupt(Hardware.Interrupt it) {
		switch (it) {
		case illegalInstruction:
			if (processScheduler.isEmpty()) {
				this.haltOS();
			} else {
				processScheduler.pop();
				this.executeProcessScheduling();
			}
			break;
		case reboot:
			int userBaseStartAddress = Hardware.Address.userBase;
			/*
			 * Start tracking read information.
			 */
			int block0 = memoryManager.getBlockIndex();
			this.memoryManager.setBlockStartAddress(userBaseStartAddress);
			/*
			 * Read the index block into primary store starting at userBase.
			 */
			readDiskBlockIntoPrimaryStore(block0, userBaseStartAddress);
			/*
			 * Start idle
			 */
			this.setIdleStart();
			break;
		case systemCall:
			int sysCallVal = simHW.fetch(Hardware.Address.systemBase);
			operatingSystemCall(sysCallVal);
			break;
		case invalidAddress:
			if (processScheduler.size() > 0) {
				processScheduler.pop();
				this.executeProcessScheduling();
			} else {
				this.haltOS();
			}
			break;
		case disk:
			/*
			 * This will execute only once, the first time the OS is booted.
			 */
			if (memoryManager.isOperatingSystemInitialized() == false) {
				this.initializeOperatingSystem();
			} else if (memoryManager.isOperatingSystemInitialized() == true) {
				if (readyRead) {
					readyRead = false;					
				} else if (readyWrite) {
					readyWrite = false;					
				}
			}
			/*
			 * This is what will trigger the call.
			 */
			this.setHardwareStatusOK();
			break;
		case terminal:
			int nValue = this.simHW.fetch(Hardware.Address.systemBase + 3); 
			numberOfCharToRead = nValue;

			int status = this.simHW.fetch(Hardware.Address.terminalStatusRegister);
			if (status == Hardware.Status.ok) {

				if (numberOfCharToRead > 0) {
					/*
					 * Copy the data from the tty data registry
					 */
					ttyData = this.simHW.fetch(Hardware.Address.terminalDataRegister);

					this.simHW.store(terminalDataStartAddress, ttyData);
					this.simHW.store(Hardware.Address.terminalDataRegister, ttyData);
				}
			} else if (status == Hardware.Status.badCommand) {
				this.setDeviceStatus(Hardware.Status.badCommand);
			}
			break;
		case countdown:
			int cDownReg = simHW.fetch(Hardware.Address.countdownRegister);
			if (cDownReg == 0) {
				this.setCountdownStart(countdownCount);
			}
			this.executeProcessScheduling();
			break;
		}
	}

	/**
	 * Operating system calls.
	 * 
	 * @param sysCall
	 */
	public void operatingSystemCall(int sysCall) {
		switch (sysCall) {
		case SystemCall.exec:
			this.osCall_Exe();
			break;
		case SystemCall.exit:
			osCall_Exit();
			break;
		case SystemCall.putSlot:
			this.osCall_PutSlot();
			break;
		case SystemCall.getSlot:
			this.osCall_GetSlot();
			break;
		case SystemCall.yield:
			this.osCall_Yield();
			break;
		case SystemCall.open:
			this.osCall_Open();
			break;
		case SystemCall.read:
			this.osCall_Read();
			break;
		case SystemCall.write:
			this.osCall_Write();
			break;
		case SystemCall.close:
			this.osCall_Close();
			break;
		case SystemCall.seek:
			this.osCall_Seek();
			break;
		case SystemCall.remove:
			this.osCall_Remove();
			break;
		}
	}

	/**
	 * OS Call Execute
	 */
	public void osCall_Exe() {
		/*
		 * The calling process requests that a new process start executing.
		 * Register 1 contains the word number of the batch-disk index block for
		 * the program associated with the new process. After the exec system
		 * call returns, the caller's register 0 contains the system call status
		 * and register 1 contains the new process's id if the exec was
		 * successful; otherwise the contents of register 1 is undefined.
		 * 
		 * The contents of registers 0 and 2 through 10 in the new process (the
		 * child process) are identical to the contents of the associated
		 * registers in the process that issued the exec (the parent process).
		 * Register 1 in the child process contains the parent process's id.
		 */
		/*
		 * Get the initial program ID.
		 */
		int programId = this.simHW.fetch(Hardware.Address.systemBase + 1);
		/*
		 * Set the hardware status to OK to trigger the call.
		 */
		this.setHardwareStatusOK();
		/*
		 * Get the status.
		 */
		int processStatus = this.simHW.fetch(Hardware.Address.systemBase);

		if (processStatus == Hardware.Status.ok) {
			/*
			 * Adds a process to the scheduler and return the processId.
			 */
			Process process = addProcessToScheduler(programId);

			if (process != null) {
				currentProcess = process;
				this.simHW.store(Hardware.Address.systemBase + 1, process.getProcessId());
			} else {
				this.setDeviceStatus(Hardware.Status.badPid);
			}
		} else if (processStatus == Hardware.Status.badPid) {
			this.setDeviceStatus(Hardware.Status.badPid);
		} else if (processStatus == Hardware.Status.noResource) {
			this.setDeviceStatus(Hardware.Status.noResource);
		}
	}

	public void osCall_Exit() {
		this.setHardwareStatusOK();
		this.setW0UndefinedOnProcessExit();
		/*
		 * Remove the currently executing process.
		 */
		processScheduler.pop();

		/*
		 * If there are no more process to remove, stop the OS.
		 */
		if (processScheduler.isEmpty()) {
			this.haltOS();
		} else {
			this.executeProcessScheduling();
		}
	}

	/**
	 * OS Call PutSlot
	 */
	public void osCall_PutSlot() {

		int putSlotReceiverId = this.simHW.fetch(Hardware.Address.systemBase + 1);
		int putSlotValue = this.simHW.fetch(Hardware.Address.systemBase + 2);
		/*
		 * This will trigger the call.
		 */
		this.setHardwareStatusOK();
		/*
		 * Get the hardwares status after the call for use in the
		 */
		int putSlotHardwareStatus = this.simHW.fetch(Hardware.Address.systemBase);
		if (putSlotHardwareStatus == Hardware.Status.ok) {
			boolean isValidProcessId = processScheduler.isValidProcessId(putSlotReceiverId);
			if (isValidProcessId) {
				/*
				 * Get the process with a given putSlotReceiverId.
				 */
				Process targetProcess = processScheduler.getProcessByProcessId(putSlotReceiverId);
				/*
				 * Set the currentProcess putSlot information if OK.
				 */
				targetProcess.setPutSlotReceiverId(putSlotReceiverId);
				targetProcess.setPutSlotValue(putSlotValue);
				/*
				 * Given value successfully written into the receiving process's
				 * slot.
				 */
				targetProcess.setSlotValueWritten(true);
				/*
				 * get the current process id and set as the sender's Id.
				 */
				int putSlotSenderId = this.currentProcess.getProcessId();
				/*
				 * sender process id.
				 */
				targetProcess.setPutSlotSenderId(putSlotSenderId);

				/*
				 * Update the process in the list.
				 */
				processScheduler.setProcessByProcessId(putSlotReceiverId, targetProcess);

				/*
				 * Block scheduler to load this process to simulator's CPU.
				 */
				this.setBlocking(true);
				/*
				 * Request process to run next.
				 */
				this.scheduleSpecificProcessForExecution(putSlotReceiverId);

			} else {
				this.setDeviceStatus(Hardware.Status.badPid);
			}
		} else if (putSlotHardwareStatus == Hardware.Status.badPid) {
			this.setDeviceStatus(Hardware.Status.badPid);
		} else if (putSlotHardwareStatus == Hardware.Status.noResource) {
			this.setDeviceStatus(Hardware.Status.noResource);
		}
	}

	/**
	 * OS Call GetSlot
	 */
	public void osCall_GetSlot() {
		/*
		 * Trigger the call.
		 */
		this.setHardwareStatusOK();
		/*
		 * Get the hardware status.
		 */
		int hardwareStatus = this.simHW.fetch(Hardware.Address.systemBase);

		if (hardwareStatus == Hardware.Status.ok) {
			int slotValue = currentProcess.getPutSlotValue();
			int senderId = currentProcess.getPutSlotSenderId();
			boolean isSlotValueWritten = currentProcess.isSlotValueWritten();

			boolean isValidProcessId = processScheduler.isValidProcessId(senderId);
			if (isValidProcessId && isSlotValueWritten) {
				this.simHW.store(Hardware.Address.systemBase + 1, slotValue);
				this.simHW.store(Hardware.Address.systemBase + 2, senderId);
				/*
				 * set that the value was read.
				 */
				currentProcess.setSlotValueRead(true);
				/*
				 * Resume scheduler.
				 */
				this.setBlocking(false);
				/*
				 * Slot value read. Reset isSlotValueWritten
				 */
				currentProcess.setSlotValueWritten(false);
				int processId = currentProcess.getProcessId();
				/*
				 * Update the current process in the list.
				 */
				this.processScheduler.setProcessByProcessId(processId, currentProcess);

			} else {
				/*
				 * The process is trying to getSlot before putSlot. The process
				 * needs to be booted.
				 */
				processScheduler.pop();
				this.executeProcessScheduling();
				this.setDeviceStatus(Hardware.Status.noResource);
			}
		} else {
			this.setDeviceStatus(Hardware.Status.noResource);
		}
	}

	/**
	 * OS Call Yield
	 */
	public void osCall_Yield() {
		this.setHardwareStatusOK();
	}

	/**
	 * OS Call Open
	 */
	public void osCall_Open() {
		int deviceType = this.simHW.fetch(Hardware.Address.systemBase + 1);

		if ((deviceType == Hardware.Terminal.device || deviceType == Hardware.Disk.device)) {
			if (deviceType == Hardware.Disk.device) {
				/*
				 * Triggers the call.
				 */
				this.setHardwareStatusOK();
				int hwStatus = this.simHW.fetch(Hardware.Address.systemBase);
				if (hwStatus == Hardware.Status.ok) {
					int getDeviceStatus = this.simHW.fetch(Hardware.Address.systemBase);
					/*
					 * Device is OK
					 */
					if (getDeviceStatus == Hardware.Status.ok) {
						ConnectionDetails cDetail = new ConnectionDetails();
						int connectionID = this.connectionDetailList.size();
						/*
						 * Store the connection ID to Word 1.
						 */
						this.simHW.store(Hardware.Address.systemBase + 1, connectionID);
						/*
						 * Get and set the connection ID
						 */
						cDetail.setConnectionID(connectionID);
						/*
						 * Connection was used to open it.
						 */
						cDetail.setConnectionOpened(true);
						cDetail.setDeviceType(deviceType);
						/*
						 * Save connection id after the call within the list.
						 */
						connectionDetailList.add(cDetail);

					} else if (getDeviceStatus == Hardware.Status.badDevice) {
						this.setDeviceStatus(Hardware.Status.badDevice);
						this.setUndefined();
					}
				} else if (hwStatus == Hardware.Status.deviceBusy) {
					this.setDeviceStatus(Hardware.Status.deviceBusy);
					this.setUndefined();
				} else if (hwStatus == Hardware.Status.badDevice) {
					this.setDeviceStatus(Hardware.Status.badDevice);
					this.setUndefined();
				}
			} else if (deviceType == Hardware.Terminal.device) {
				terminalInUse = terminalUseStack.contains(deviceType);
				/*
				 * Triggers the call.
				 */
				this.setHardwareStatusOK();
				int terminalHWStatus = this.simHW.fetch(Hardware.Address.systemBase);
				if (terminalHWStatus == Hardware.Status.ok) {
					if (terminalInUse == false) {
						ConnectionDetails cnDetail = new ConnectionDetails();
						int connectionID = this.connectionDetailList.size();
						/*
						 * Set the connection ID.
						 */
						this.simHW.store(Hardware.Address.systemBase + 1, connectionID);
						// this.simHW.store(Hardware.Address.systemBase,
						// Hardware.Status.ok);

						cnDetail.setConnectionID(connectionID);
						/*
						 * Connection was used to open it.
						 */
						cnDetail.setConnectionOpened(true);
						/*
						 * Store the connection type.
						 */
						cnDetail.setDeviceType(deviceType);
						/*
						 * Save connection id after the call within the list.
						 */
						connectionDetailList.add(cnDetail);
						/*
						 * Set the terminal in use status.
						 */
						terminalUseStack.push(deviceType);

					} else if (terminalInUse) {

						this.setDeviceStatus(Hardware.Status.deviceBusy);
					}
				} else if (terminalHWStatus == Hardware.Status.deviceBusy) {
					this.setDeviceStatus(Hardware.Status.deviceBusy);
				} else if (terminalHWStatus == Hardware.Status.badDevice) {
					this.setDeviceStatus(Hardware.Status.badDevice);
				} else {
					this.setDeviceStatus(-1);
				}
			}
		} else if (deviceType == OperatingSystem.FileIO.file) {
			/*
			 * File open stuff.
			 */
			int fileTag = this.simHW.fetch(Hardware.Address.systemBase + 3);

			/*
			 * File flags.
			 */
			// List<Integer> fileFlags = digitList(flags);

			/*
			 * Triggers the call.
			 */
			this.setHardwareStatusOK();

			int hwStatus = this.simHW.fetch(Hardware.Address.systemBase);

			if (hwStatus == Hardware.Status.ok) {
				boolean isIndexOutOfBounds = this.fileList.isIndexOutOfBounds(fileTag);
				if (fileList.isEmpty() == false && isIndexOutOfBounds == false) {

				} else {
					// this.setDeviceStatus(Hardware.Status.deviceBusy);
					// this.setUndefined();
					this.haltOS();
				}
			} else if (hwStatus == Hardware.Status.deviceBusy) {

				this.setDeviceStatus(Hardware.Status.deviceBusy);
			} else if (hwStatus == Hardware.Status.badDevice) {
				this.setDeviceStatus(Hardware.Status.badDevice);
			}
		}
	}
	
	/**
	 * OS Call Read
	 */
	public void osCall_Read() {
		int readConnectionId = this.simHW.fetch(Hardware.Address.systemBase + 1);
		int beforeCallReadReadToAddress = this.simHW.fetch(Hardware.Address.systemBase + 2) + 1;
		int beforeCallRead_N_Value = this.simHW.fetch(Hardware.Address.systemBase + 3);
		/*
		 * This is what will trigger the call.
		 */
		this.setHardwareStatusOK();
		/*
		 * Get the connection info based on the details.
		 */
		int cnnDetailListSize = connectionDetailList.size();

		if (readConnectionId < cnnDetailListSize && readConnectionId >= 0) {
			int deviceReadStatus = this.simHW.fetch(Hardware.Address.systemBase);
			if (deviceReadStatus == Hardware.Status.ok) {
				int afterCallReadNValue = this.simHW.fetch(Hardware.Address.systemBase + 1);
				ConnectionDetails cnnDetails = connectionDetailList.get(readConnectionId);
				cnnDetails.setReadTo_N_Value(beforeCallRead_N_Value);
				cnnDetails.setReadToAddress(beforeCallReadReadToAddress);
				cnnDetails.setReadyRead(true);
				connectionDetailList.set(readConnectionId, cnnDetails);

				if (cnnDetails.getDeviceType() == Hardware.Disk.device) {
					if (beforeCallRead_N_Value >= 0) {
						if (beforeCallReadReadToAddress >= 0) {
							this.readDiskBlockIntoPrimaryStore(beforeCallRead_N_Value, beforeCallReadReadToAddress);
						} else {
							this.setDeviceStatus(Hardware.Status.badAddress);
						}
					} else {
						this.setDeviceStatus(Hardware.Status.badCount);
					}
				} else if (cnnDetails.getDeviceType() == Hardware.Terminal.device) {
					terminalDataStartAddress = beforeCallReadReadToAddress;
					numberOfCharToRead = afterCallReadNValue;

					if (afterCallReadNValue >= 0) {
						if (beforeCallReadReadToAddress >= 0) {
							if (ttyData == Hardware.Terminal.eosCharacter) {

								this.simHW.store(Hardware.Address.systemBase + 1, 0);
								this.haltOS();
							} else {
								this.simHW.store(Hardware.Address.terminalCommandRegister, Hardware.Terminal.readCommand);
								this.simHW.store(Hardware.Address.systemBase + 1, beforeCallRead_N_Value);
							}
						}
					} else {
						this.simHW.store(Hardware.Address.systemBase, Hardware.Status.badCount);
					}
				}
				/*
				 * The disk read operation is ready.
				 */
				this.readyRead = true;

			} else if (deviceReadStatus == Hardware.Status.badDevice) {
				this.setDeviceStatus(Hardware.Status.badDevice);
			} else if (deviceReadStatus == Hardware.Status.badAddress) {
				this.setDeviceStatus(Hardware.Status.badAddress);
			} else if (deviceReadStatus == Hardware.Status.badCount) {
				this.setDeviceStatus(Hardware.Status.badCount);
			}
		} else {
			this.setDeviceStatus(Hardware.Status.badDevice);
		}
	}

	/**
	 * OS Call Write
	 */
	public void osCall_Write() {

		int beforeWriteConnectionID = this.simHW.fetch(Hardware.Address.systemBase + 1);
		int beforeWriteWriteFromAddress = this.simHW.fetch(Hardware.Address.systemBase + 2);
		int beforeWrite_N_Value = this.simHW.fetch(Hardware.Address.systemBase + 3);

		int cnnDetailListSize = connectionDetailList.size();
		if (beforeWriteConnectionID < cnnDetailListSize && beforeWriteConnectionID >= 0) {
			/*
			 * This is what will trigger the call.
			 */
			this.setHardwareStatusOK();
			int hwStatus = this.simHW.fetch(Hardware.Address.systemBase);
			if (hwStatus == Hardware.Status.ok) {

				ConnectionDetails cnnDetails = this.connectionDetailList.get(beforeWriteConnectionID);
				cnnDetails.setWriteFrom_N_Value(beforeWrite_N_Value);
				cnnDetails.setWriteFromAddress(beforeWriteWriteFromAddress);

				if (cnnDetails.getDeviceType() == Hardware.Disk.device) {
					if (beforeWrite_N_Value >= 0) {
						if (beforeWriteWriteFromAddress >= 0) {
							/*
							 * Connection is ready to write.
							 */
							cnnDetails.setReadyWrite(true);
							/*
							 * Save the write connection information.
							 */
							this.connectionDetailList.set(beforeWriteConnectionID, cnnDetails);

							if (cnnDetails.getDeviceType() == Hardware.Disk.device) {
								this.writeDiskBlockIntoPrimaryStore(beforeWrite_N_Value, beforeWriteWriteFromAddress);
							}
							this.readyWrite = true;
						} else {
							this.setDeviceStatus(Hardware.Status.badAddress);
						}
					} else {
						this.setDeviceStatus(Hardware.Status.badCount);
					}
				} else if (cnnDetails.getDeviceType() == Hardware.Terminal.device) {

					if (ttyData != Hardware.Terminal.eosCharacter) {
						this.simHW.store(Hardware.Address.terminalCommandRegister, Hardware.Terminal.writeCommand);
					} 
				}
			} else if (hwStatus == Hardware.Status.badDevice) {
				this.setDeviceStatus(Hardware.Status.badDevice);
			} else if (hwStatus == Hardware.Status.badAddress) {
				this.setDeviceStatus(Hardware.Status.badAddress);
			} else if (hwStatus == Hardware.Status.badCount) {
				this.setDeviceStatus(Hardware.Status.badCount);
			} else if (hwStatus == Hardware.Status.badPid) {
				this.setDeviceStatus(Hardware.Status.badPid);
			}
		} else {
			this.setDeviceStatus(Hardware.Status.badDevice);
		}
	}

	/**
	 * OS Call Close
	 */
	public void osCall_Close() {
		int closeConnectionID = this.simHW.fetch(Hardware.Address.systemBase + 1);
		/*
		 * This is what will trigger the call.
		 */
		this.setHardwareStatusOK();
		/*
		 * Get the device status.
		 */
		int deviceStatus = this.simHW.fetch(Hardware.Address.systemBase);

		if (deviceStatus == Hardware.Status.ok) {
			boolean indexOutOfBounds = indexOutOfBounds(connectionDetailList, closeConnectionID);
			if (indexOutOfBounds == false) {
				ConnectionDetails cnDetail = this.connectionDetailList.get(closeConnectionID);

				if (cnDetail.isConnectionClosed() == false) {
					int deviceType = cnDetail.getDeviceType();
					if (deviceType == Hardware.Disk.device) {
						/*
						 * Set the connection status to closed.
						 */
						cnDetail.setConnectionClosed(true);

						/*
						 * Set hardware status to OK.
						 */
						this.setHardwareStatusOK();
					} else if (deviceType == Hardware.Terminal.device) {
						/*
						 * Is the terminal being used for it to be closed?
						 */
						terminalInUse = terminalUseStack.contains(deviceType);

						if (terminalInUse) {
							/*
							 * Pop the terminal stack to allow for it to be used
							 * again.
							 */
							terminalUseStack.pop();
							/*
							 * Set the connection status to closed.
							 */
							cnDetail.setConnectionClosed(true);

							/*
							 * Set hardware status to OK.
							 */
							this.setHardwareStatusOK();
							/*
							 * Resets the timer count-down, so it does not get
							 * an illegal instruction.
							 */
							this.setCountdownStart();
						} else {
							this.setDeviceStatus(Hardware.Status.badDevice);
							this.setUndefined();
						}
					}
				} else if (cnDetail.isConnectionClosed() == true) {
					this.setDeviceStatus(Hardware.Status.badDevice);
					this.setUndefined();
				}
			} else {
				this.setDeviceStatus(Hardware.Status.badDevice);
				this.setUndefined();
			}
		} else if (deviceStatus == Hardware.Status.badDevice) {
			this.setDeviceStatus(Hardware.Status.badDevice);
			this.setUndefined();
		}
	}

	/**
	 * OS Call Seek
	 */
	public void osCall_Seek() {
		/*
		 * This is what will trigger the call.
		 */
		this.setHardwareStatusOK();
	}

	/**
	 * OS Call Remove
	 */
	public void osCall_Remove() {
		/*
		 * This is what will trigger the call.
		 */
		this.setHardwareStatusOK();
	}

	/**
	 * Initializes OS
	 */
	private void initializeOperatingSystem() {
		int exeProgramBlockCount = getExeProgsBlockCount();
		if (exeProgramBlockCount == 0) {
			this.haltOS();
		}

		int totalBlockCount = exeProgramBlockCount + 1;
		int nextBlkIndex = memoryManager.getBlockIndex() + 1;
		/*
		 * Index block is loaded on boot. Load the program blocks by counting
		 * blocks in the index block.
		 */
		if (nextBlkIndex < totalBlockCount) {
			if (nextBlkIndex < maxUsrSpaceBlockCount) {

				int nextBlockIndex = this.memoryManager.getBlockIndex() + 1;
				int nextBlockStartAddress = simHW.fetch(Hardware.Address.diskAddressRegister) + 32;

				this.memoryManager.setBlockIndex(nextBlockIndex);
				this.memoryManager.setBlockStartAddress(nextBlockStartAddress);

				this.readDiskBlockIntoPrimaryStore(nextBlockIndex, nextBlockStartAddress);
			} else if (nextBlkIndex == maxUsrSpaceBlockCount) {

				int nextBlockIndex = this.memoryManager.getBlockIndex() + 1;
				/*
				 * Start loading blocks greater than 32 in the system space.
				 */
				int systemBaseStartAddress = Hardware.Address.systemBase;
				int nextBlockStartAddress = systemBaseStartAddress + 32;

				this.memoryManager.setBlockIndex(nextBlockIndex);
				this.memoryManager.setBlockStartAddress(nextBlockStartAddress);

				this.readDiskBlockIntoPrimaryStore(nextBlockIndex, nextBlockStartAddress);
			} else if (nextBlkIndex > maxUsrSpaceBlockCount) {

				int nextBlockIndex = this.memoryManager.getBlockIndex() + 1;
				int nextBlockStartAddress = simHW.fetch(Hardware.Address.diskAddressRegister) + 32;

				this.memoryManager.setBlockIndex(nextBlockIndex);
				this.memoryManager.setBlockStartAddress(nextBlockStartAddress);

				this.readDiskBlockIntoPrimaryStore(nextBlockIndex, nextBlockStartAddress);
			}
		}

		/*
		 * Program entity loading here. Used to request program execution.
		 */
		if (nextBlkIndex == totalBlockCount && memoryManager.isProgramLoadingComplete() == false) {

			memoryManager.setProgramLoadingComplete(true);

			memoryManager.setProgramLoadingComplete(true);
			/*
			 * Create the user space entity.
			 */
			this.createUserSpaceDiskEntity();
			/*
			 * Create the system space entity.
			 */
			this.createSystemSpaceDiskEntity();
			/*
			 * Load the index block with program information to queue up program
			 * information.
			 */
			List<WordEntity> iEntity = userSpaceDiskEntity.getBlockEntity(0).getWordEntityList();
			this.queueProcessExecution(iEntity);
			/*
			 * Must only run once.
			 */
			this.setCountdownStart(countdownCount);
		}

		if (memoryManager.isProgramLoadingComplete() == true && memoryManager.isFirstProgramStarted() == false) {
			/*
			 * All index and program blocks loaded. It is time to execute the
			 * first program.
			 */
			if (memoryManager.isFirstProgramStarted() == false) {

				int programId = 0;
				/*
				 * Execute the program.
				 */
				programExeRequest(programId);
				/*
				 * Adds a process to the scheduler
				 */
				currentProcess = addProcessToScheduler(programId);

				memoryManager.setFirstProgramStarted(true);

				memoryManager.setOperatingSystemInitialized(true);
				this.setCountdownStart(countdownCount);
			}
		}
	}

	/**
	 * Add a process to scheduler.
	 * 
	 * @param programId
	 * @param processId
	 */
	private Process addProcessToScheduler(int programId) {
		/*
		 * Get the next process info based on the program index.
		 */
		BlockEntity requestedProcess = getProgramBlockEntity(programId);

		if (requestedProcess != null) {
			/*
			 * Get the next process ID. processIndex starts at 1.
			 */
			int processId = processIndex++;
			Process process = new Process(processId, processQuntum);
			process.setParentProgramID(programId);
			process.setProcessPCRegister(requestedProcess.getPCRegister());
			process.setProcessBaseRegister(requestedProcess.getBaseRegister());
			process.setProcessTopRegister(requestedProcess.getTopRegister());
			process.setProcessId(processId);
			/*
			 * Add to scheduler
			 */
			processScheduler.add(process);

			return process;
		} else {
			return null;
		}
	}

	/**
	 * Loads a disk block into primary store.
	 * 
	 * @param blockIndex
	 * @param blockStartAddress
	 */
	private void readDiskBlockIntoPrimaryStore(int blockIndex, int blockStartAddress) {
		this.simHW.store(Hardware.Address.diskBlockRegister, blockIndex);
		this.simHW.store(Hardware.Address.diskAddressRegister, blockStartAddress);
		this.simHW.store(Hardware.Address.diskCommandRegister, Hardware.Disk.readCommand);
	}

	private void writeDiskBlockIntoPrimaryStore(int blockIndex, int blockStartAddress) {
		this.simHW.store(Hardware.Address.diskBlockRegister, blockIndex);
		this.simHW.store(Hardware.Address.diskAddressRegister, blockStartAddress);
		this.simHW.store(Hardware.Address.diskCommandRegister, Hardware.Disk.writeCommand);
	}

	/**
	 * Process control block scheduling.
	 */
	private void executeProcessScheduling() {
		if (processScheduler.size() > 1024) {
			this.haltOS();
		}

		if (this.isBlocking() == false) {
			Process nextProcess;
			boolean isEmpty = processScheduler.isEmpty();
			if (isEmpty) {

				this.haltOS();
			} else {
				if (processScheduler.size() > 0) {
					processScheduler.advance();
					nextProcess = processScheduler.getFirst();
					processExeRequest(nextProcess);
					/*
					 * Make the current executing process the current process.
					 */
					this.currentProcess = nextProcess;
				}
			}
		}
	}

	/*
	 * Queues requested process for execution.
	 */
	private Process scheduleSpecificProcessForExecution(int requestedProcessId) {
		Process processReturn = null;
		boolean isEmpty = processScheduler.isEmpty();
		if (isEmpty) {

			this.haltOS();
		} else {
			if (processScheduler.size() > 0) {
				/*
				 * Move to the next process.
				 */
				processScheduler.advance();
				RoundRobinScheduler rRS = processScheduler;
				for (Process process : rRS) {
					if (process.getProcessId() == requestedProcessId) {
						currentProcess = process;
						processExeRequest(currentProcess);
						if (exeCount > 1024) {
							this.haltOS();
						}

						exeCount++;
						return currentProcess;
					} else {
						processScheduler.advance();
					}
				}
			}
		}
		return processReturn;
	}

	/**
	 * Get the total program block count.
	 * 
	 * @return
	 */
	private int getExeProgsBlockCount() {
		int blkCount = 0;
		for (int i = 0; i < Hardware.Disk.blockSize; i++) {
			int programBlocks = simHW.fetch(Hardware.Address.userBase + i);
			if (programBlocks != 0) {
				blkCount += programBlocks;
			}
		}
		return blkCount;
	}
	
	/**
	 * Creates the disk blocks to reference program block info in user space
	 */
	private void createUserSpaceDiskEntity() {
		List<WordEntity> wordEnt = new ArrayList<WordEntity>();

		for (int i = Hardware.Address.userBase; i < Hardware.Address.userTop - 1; i++) {
			WordEntity wEnt = new WordEntity();
			wEnt.setWordAddress(i);
			wordEnt.add(wEnt);
		}

		List<WordEntity> wordList = Collections.unmodifiableList(wordEnt);
		List<List<WordEntity>> subParts = createBlockList(wordList, 32);

		for (int i = 0; i < subParts.size(); i++) {
			BlockEntity blkEnt = new BlockEntity();
			blkEnt.setWordEntityList(subParts.get(i));
			userSpaceDiskEntity.getBlockEntityList().add(blkEnt);
		}
	}

	/**
	 * Creates the disk blocks to reference program block info in system space
	 */
	private void createSystemSpaceDiskEntity() {
		List<WordEntity> wordEnt = new ArrayList<WordEntity>();

		for (int i = Hardware.Address.systemBase; i < Hardware.Address.systemTop - 1; i++) {
			WordEntity wEnt = new WordEntity();
			wEnt.setWordAddress(i);
			wordEnt.add(wEnt);
		}

		List<WordEntity> wordList = Collections.unmodifiableList(wordEnt);
		List<List<WordEntity>> subParts = createBlockList(wordList, 32);

		for (int i = 0; i < subParts.size(); i++) {
			BlockEntity blkEnt = new BlockEntity();
			blkEnt.setWordEntityList(subParts.get(i));
			systemSpaceDiskEntity.getBlockEntityList().add(blkEnt);
		}
	}

	/**
	 * Create block list.
	 * 
	 * @param list
	 * @param L
	 * @return
	 */
	private static <T> List<List<T>> createBlockList(List<T> list, final int L) {
		List<List<T>> parts = new ArrayList<List<T>>();
		final int N = list.size();
		for (int i = 0; i < N; i += L) {
			parts.add(new ArrayList<T>(list.subList(i, Math.min(N, i + L))));
		}
		return parts;
	}

	private void loadUserSpaceProgramBlocks(int firstBlockIndex, int lastBlockIndex, int processBlockCount) {
		int processStartBlockAddress = userSpaceDiskEntity.getBlockEntityList().get(firstBlockIndex).getWordEntityList().get(0).getWordAddress() + 32;
		int processEndBlockAddress = userSpaceDiskEntity.getBlockEntityList().get(lastBlockIndex).getWordEntityList().get(0).getWordAddress() + 32;

		/*
		 * The hardware interprets the contents of the Base Register as the
		 * lowest legal address accessible to the currently running process.
		 */
		int baseRegister = processStartBlockAddress;

		/*
		 * The hardware interprets the contents of the Top Register as one more
		 * than the highest legal address accessible to the currently running
		 * process.
		 */
		int topRegister = processEndBlockAddress + 1;

		/*
		 * The hardware interprets the contents of the Program Counter Register
		 * as the address of the next instruction to execute.
		 */
		int pCRegister = processStartBlockAddress;

		this.setUserSpaceProgramBlockList(processStartBlockAddress, processBlockCount, pCRegister, baseRegister, topRegister);
	}

	private void loadSystemSpaceProgramBlocks(int firstBlockIndex, int lastBlockIndex, int processBlockCount) {
		int processStartBlockAddress = systemSpaceDiskEntity.getBlockEntityList().get(firstBlockIndex).getWordEntityList().get(0).getWordAddress() + 32;
		int processEndBlockAddress = systemSpaceDiskEntity.getBlockEntityList().get(lastBlockIndex).getWordEntityList().get(0).getWordAddress() + 32;

		/*
		 * The hardware interprets the contents of the Base Register as the
		 * lowest legal address accessible to the currently running process.
		 */
		int baseRegister = processStartBlockAddress;

		/*
		 * The hardware interprets the contents of the Top Register as one more
		 * than the highest legal address accessible to the currently running
		 * process.
		 */
		int topRegister = processEndBlockAddress + 1;

		/*
		 * The hardware interprets the contents of the Program Counter Register
		 * as the address of the next instruction to execute.
		 */
		int pCRegister = processStartBlockAddress;

		this.setSystemSpaceProgramBlockList(processStartBlockAddress, processBlockCount, pCRegister, baseRegister, topRegister);
	}

	/**
	 * Shows how to get to the first and last block of each process based on the
	 * index block(0).
	 * 
	 * @param nBlock
	 */
	private void queueProcessExecution(List<WordEntity> iBlock) {
		int exeProgramBlockCount = getExeProgsBlockCount();
		int firstBlockIndex = 1;
		int lastBlockIndex = 0;
		int usedBlockCount = 0;
		for (int i = 0; i < iBlock.size(); i++) {
			int proAddr = iBlock.get(i).getWordAddress();
			int processBlockCount = this.simHW.fetch(proAddr);
			if (processBlockCount != 0) {
				int totalBlockCount = exeProgramBlockCount + 1;
				if (usedBlockCount < 31) {
					firstBlockIndex = lastBlockIndex;
					lastBlockIndex = firstBlockIndex + processBlockCount;
					loadUserSpaceProgramBlocks(firstBlockIndex, lastBlockIndex, processBlockCount);
					usedBlockCount += processBlockCount;
				} else if (usedBlockCount == 31) {
					firstBlockIndex = 1;
					lastBlockIndex = firstBlockIndex + processBlockCount;
					loadSystemSpaceProgramBlocks(firstBlockIndex, lastBlockIndex, processBlockCount);
					usedBlockCount += processBlockCount;
				} else if (usedBlockCount < totalBlockCount) {
					firstBlockIndex = lastBlockIndex;
					lastBlockIndex = firstBlockIndex + processBlockCount;
					loadSystemSpaceProgramBlocks(firstBlockIndex, lastBlockIndex, processBlockCount);
					usedBlockCount += processBlockCount;
				} else {
					this.haltOS();
				}
			}
		}
	}

	/**
	 * Creates a list containing all of the blocks within one program into user
	 * space.
	 * 
	 * @param programStartBlock
	 * @param processBlockCount
	 * @param pCRegister
	 * @param baseRegister
	 * @param topRegister
	 */
	private void setUserSpaceProgramBlockList(int programStartBlock, int processBlockCount, int pCRegister, int baseRegister, int topRegister) {
		BlockEntity bEnt = new BlockEntity();
		for (int proBlockIndex = programStartBlock; proBlockIndex <= processBlockCount; proBlockIndex++) {
			List<WordEntity> iBlock = userSpaceDiskEntity.getBlockEntityList().get(proBlockIndex).getWordEntityList();
			for (int j = 0; j < iBlock.size(); j++) {
				bEnt.getWordEntityList().add(iBlock.get(j));
			}
		}
		bEnt.setBaseRegister(baseRegister);
		bEnt.setTopRegister(topRegister);
		bEnt.setPCRegister(pCRegister);
		programEntity.getBlockEntityList().add(bEnt);

	}

	/**
	 * Creates a list containing all of the blocks within one program into
	 * system space.
	 * 
	 * @param programStartBlock
	 * @param processBlockCount
	 * @param pCRegister
	 * @param baseRegister
	 * @param topRegister
	 */
	private void setSystemSpaceProgramBlockList(int programStartBlock, int processBlockCount, int pCRegister, int baseRegister, int topRegister) {
		BlockEntity bEnt = new BlockEntity();
		for (int proBlockIndex = programStartBlock; proBlockIndex <= processBlockCount; proBlockIndex++) {
			List<WordEntity> iBlock = systemSpaceDiskEntity.getBlockEntityList().get(proBlockIndex).getWordEntityList();
			for (int j = 0; j < iBlock.size(); j++) {
				bEnt.getWordEntityList().add(iBlock.get(j));
			}
		}
		bEnt.setBaseRegister(baseRegister);
		bEnt.setTopRegister(topRegister);
		bEnt.setPCRegister(pCRegister);
		programEntity.getBlockEntityList().add(bEnt);

	}

	/**
	 * Process a program based on the range found in a program.
	 * 
	 * @param index
	 */
	private void programExeRequest(int index) {
		int progCount = this.programEntity.getBlockEntityList().size();
		if (index < progCount) {
			int pCRegister = programEntity.getBlockEntityList().get(index).getPCRegister();
			int baseRegister = programEntity.getBlockEntityList().get(index).getBaseRegister();
			int topRegister = programEntity.getBlockEntityList().get(index).getTopRegister();

			this.simHW.store(Hardware.Address.baseRegister, baseRegister);
			this.simHW.store(Hardware.Address.topRegister, topRegister);
			this.simHW.store(Hardware.Address.PCRegister, pCRegister);
		} else {
			simHW.store(Hardware.Address.haltRegister, 2);
		}
	}

	/**
	 * Get a program information based on program index.
	 * 
	 * @param index
	 * @return
	 */
	private BlockEntity getProgramBlockEntity(int index) {
		boolean indexOutOfBounds = indexOutOfBounds(programEntity.getBlockEntityList(), index);
		if (indexOutOfBounds == false) {
			return programEntity.getBlockEntityList().get(index);
		} else {
			return null;
		}
	}

	private void setDeviceStatus(int status) {
		this.simHW.store(Hardware.Address.systemBase, status);
	}

	/**
	 * Does the index exist?
	 * 
	 * @param list
	 * @param index
	 * @return
	 */
	public boolean indexOutOfBounds(final List<?> list, final int index) {
		return (index < 0 || index >= list.size());
	}

	/**
	 * Execute process execution request.
	 * 
	 * @param process
	 */
	private void processExeRequest(Process process) {

		int processPCRegister = process.getProcessPCRegister();
		int processBaseRegister = process.getProcessBaseRegister();
		int processTopRegister = process.getProcessTopRegister();

		this.simHW.store(Hardware.Address.baseRegister, processBaseRegister);
		this.simHW.store(Hardware.Address.topRegister, processTopRegister);
		this.simHW.store(Hardware.Address.PCRegister, processPCRegister);
	}

	public boolean isBlocking() {
		return blocking;
	}

	public void setBlocking(boolean blocking) {
		this.blocking = blocking;
	}
}